﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Xml.Linq;

namespace SeoulOpenApi
{
	public class BaseService
	{
		/// <summary>
		/// 서울시 API 서비스
		/// </summary>
		public BaseService() : this(null, null) { }

		/// <summary>
		/// 서울시 API 서비스
		/// </summary>
		/// <param name="userId">서울시 API접속 ID</param>
		/// <param name="authKey">서울시로부터 발급받은 인증키</param>
		/// <remarks>인증키 발급: http://mobile.openapi.seoul.go.kr/ </remarks>
		public BaseService(string userId, string authKey)
		{
			UserID = userId;
			AuthKey = authKey;
		}

		#region Public
		/// <summary>
		/// 서울시 API접속 ID
		/// </summary>
		public string UserID { get; protected internal set; }
		/// <summary>
		/// 서울시로부터 발급받은 인증키
		/// </summary>
		/// <remarks>인증키 발급: http://mobile.openapi.seoul.go.kr/ </remarks>
		public string AuthKey { get; protected internal set; }

		/// <summary>
		/// 서울시 API 주소의 공통부분
		/// <para>"http://mobile.openapi.seoul.go.kr/SeoulOpenAPI/services/"</para>
		/// </summary>
		public static string ApiUrlPrefix
		{
			get { return @"http://mobile.openapi.seoul.go.kr/SeoulOpenAPI/services/"; }
		}

		/// <summary>
		/// API의 정보 수신이 완료되었을 시 발생하는 이벤트
		/// </summary>
		public event DownloadDelegate OnDownloadCompleted;


		/// <summary>
		/// Soap 서비스 정보 클래스
		/// </summary>
		public class SoapData
		{
			/// <summary>
			/// 고유한 이름
			/// </summary>
			public string Name { get; set; }
			/// <summary>
			/// SOAP WSDL 주소
			/// </summary>
			public Uri Address { get; set; }
			/// <summary>
			/// 설명
			/// </summary>
			public string Description { get; set; }
			/// <summary>
			/// 보내는 Soap Body에 넣을 XML 문자열. 인자(args)가 있는 경우 {0},{1} 식으로 값과 대체할 슬롯이 있어야 함.
			/// </summary>
			public string SoapBody { get; set; }
		}

		/// <summary>
		/// 서비스 정보를 받아옴
		/// </summary>
		/// <param name="serviceName">서비스명 (주의! 대소문자 구분함)</param>
		/// <param name="removeNamespace">네임스페이스 제거 여부 (기본값 true)</param>
		/// <exception cref="KeyNotFoundException">서비스 이름이 목록에서 없을 경우</exception>
		/// <exception cref="ArgumentNullException">USER_ID 또는 AUTH_KEY가 null일 경우</exception>
		public void Request(string serviceName, bool removeNamespace = true)
		{
			Request(serviceName, (string)null);
		}

		/// <summary>
		/// 서비스 정보를 받아옴
		/// </summary>
		/// <param name="serviceName">서비스명 (주의! 대소문자 구분함)</param>
		/// <param name="arg">입력값</param>
		/// <param name="removeNamespace">네임스페이스 제거 여부 (기본값 true)</param>
		/// <exception cref="KeyNotFoundException">서비스 이름이 목록에서 없을 경우</exception>
		/// <exception cref="ArgumentNullException">USER_ID 또는 AUTH_KEY가 null일 경우</exception>
		public void Request(string serviceName, string arg, bool removeNamespace = true)
		{
			Request(serviceName, new[] { arg });
		}

		/// <summary>
		/// 서비스 정보를 받아옴
		/// </summary>
		/// <param name="serviceName">서비스명</param>
		/// <param name="args">입력값 (여러개) <code>new[] {args1, args2} 식으로 사용 가능함</code></param>
		/// <param name="removeNamespace">네임스페이스 제거 여부 (기본값 true)</param>
		/// <exception cref="KeyNotFoundException">서비스 이름이 목록에서 없을 경우</exception>
		/// <exception cref="ArgumentNullException">USER_ID 또는 AUTH_KEY가 null일 경우</exception>
		public void Request(string serviceName, string[] args, bool removeNamespace = true)
		{
			// Validation
			if (String.IsNullOrEmpty(UserID))
				throw new ArgumentNullException(UserID);
			if (String.IsNullOrEmpty(AuthKey))
				throw new ArgumentNullException(AuthKey);

			// 통신오브젝트
			var soap = getSoapData(serviceName);
			var webClient = new WebClient();

			// 헤더 추가. SOAPAction 헤더는 없어도 동작에는 상관없어서 제외.
			webClient.Headers["Content-Type"] = "application/soap+xml; charset=utf-8";

			// 다운로드 완료되면 이벤트를 외부로 발사
			if (removeNamespace) // 네임스페이스 제거 옵션
				webClient.UploadStringCompleted += (s, e) => OnDownloadCompleted(e.Error == null, removeXmlNamespace(XElement.Parse(e.Result)));
			else
				webClient.UploadStringCompleted += (s, e) => OnDownloadCompleted(e.Error == null, XElement.Parse(e.Result));

			// Soap 메세지스트링 생성
			var soapMessage = buildSoapEnvelope(soap, args);
			// 비동기 실행
			webClient.UploadStringAsync(soap.Address, "POST", soapMessage);
		}

		#endregion Public

		/// <summary>
		/// Soap 메세지를 구성
		/// </summary>
		/// <param name="data"></param>
		/// <param name="args"></param>
		/// <returns></returns>
		private string buildSoapEnvelope(SoapData data, string[] args = null)
		{
			var prefix = "<?xml version='1.0' encoding='utf-8'?>" +
						 "<soapenv:Envelope xmlns:soapenv='http://www.w3.org/2003/05/soap-envelope'>" +
						 "<soapenv:Header>" +
						 "<authInfo:userId xmlns:authInfo='{0}'>{1}</authInfo:userId>" +
						 "<authInfo:authKey xmlns:authInfo='{0}'>{2}</authInfo:authKey>" +
						 "</soapenv:Header>" +
						 "<soapenv:Body>";

			// Prefix 값 치환.
			prefix = String.Format(prefix, data.Address, UserID, AuthKey);

			var body = args != null ? String.Format(data.SoapBody, args) : data.SoapBody;

			const string suffix = "</soapenv:Body>" +
								  "</soapenv:Envelope>";

			// 메세지 결합
			return prefix + body + suffix;
		}


		/// <summary>
		/// 네임스페이스를 제거하여 깔끔한 형태로 만들어줌
		/// </summary>
		/// <param name="xml">입력 XML</param>
		/// <returns></returns>
		private static XElement removeXmlNamespace(XElement xml)
		{
			// http://stackoverflow.com/questions/987135/how-to-remove-all-namespaces-from-xml-with-c
			if (!xml.HasElements)
			{
				var xElement = new XElement(xml.Name.LocalName) { Value = xml.Value };
				return xElement;
			}
			return new XElement(xml.Name.LocalName, xml.Elements().Select(removeXmlNamespace)); // recursive loop
		}

		/// <summary>
		/// 서비스 항목에 따라 데이터 가져오기
		/// </summary>
		/// <param name="name">서비스명</param>
		/// <returns></returns>
		protected internal virtual SoapData getSoapData(string name)
		{
			throw new NotImplementedException("Inheritance has to be implemented this method.");
		}

		#region Backing Fields
		public delegate void DownloadDelegate(bool isSuccess, XElement result);
		#endregion Backing Fields

	}
}
